Add classical code generation workflow for typed event and RPC classes#79
Closed
Add classical code generation workflow for typed event and RPC classes#79
Conversation
Introduces a TypeScript-based code generator (scripts/codegen/java.ts) that reads api.schema.json and session-events.schema.json to produce: - Typed session event classes (sealed hierarchy under AbstractSessionEvent) - Typed RPC wrapper classes (ServerRpc, SessionRpc) for JSON-RPC methods - Jackson-annotated records with @JsonCreator, @JsonProperty, @JsonInclude Migrates the hand-written events package to auto-generated types, wires the generated RPC wrappers into CopilotClient and CopilotSession, and adds comprehensive tests including E2E coverage. Also includes: Windows CLI path resolution fixes, shared TestUtil extraction, lazy getRpc() initialization, race condition fix in SessionEventsE2ETest, and a guard preventing agentic sync from modifying src/generated/java/ files.
6 tasks
Contributor
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Introduces a TypeScript-based code generation workflow that emits strongly-typed Java session event classes and RPC wrappers from Copilot CLI JSON schemas, replacing the hand-written events package and moving SDK internals to typed RPC calls.
Changes:
- Add
scripts/codegen/java.ts+ npm package to generatesrc/generated/java/event and RPC classes. - Update SDK runtime to deserialize events via generated
SessionEventand to invoke RPC methods through typedServerRpc/SessionRpc. - Add CI workflows to verify generated sources and to automate
@github/copilotschema dependency bumps.
Show a summary per file
| File | Description |
|---|---|
| src/site/markdown/advanced.md | Updates documentation links and examples to use generated event types (SessionEvent, SessionModelChangeEvent). |
| src/main/java/com/github/copilot/sdk/package-info.java | Updates package docs to reference the new generated package for events. |
| src/main/java/com/github/copilot/sdk/json/SessionConfig.java | Changes pre-session event hook type from AbstractSessionEvent to generated SessionEvent. |
| src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java | Changes resume-session event hook type from AbstractSessionEvent to generated SessionEvent. |
| src/main/java/com/github/copilot/sdk/events/package-info.java | Removes legacy hand-written events package documentation (replaced by generated events). |
| src/main/java/com/github/copilot/sdk/events/UserMessageEvent.java | Deletes legacy hand-written event DTO (replaced by generated equivalent). |
| src/main/java/com/github/copilot/sdk/events/UnknownSessionEvent.java | Deletes legacy unknown-event DTO (replaced by generated equivalent). |
| src/main/java/com/github/copilot/sdk/events/ToolUserRequestedEvent.java | Deletes legacy hand-written event DTO (replaced by generated equivalent). |
| src/main/java/com/github/copilot/sdk/events/ToolExecutionStartEvent.java | Deletes legacy hand-written event DTO (replaced by generated equivalent). |
| src/main/java/com/github/copilot/sdk/events/ToolExecutionProgressEvent.java | Deletes legacy hand-written event DTO (replaced by generated equivalent). |
| src/main/java/com/github/copilot/sdk/events/ToolExecutionPartialResultEvent.java | Deletes legacy hand-written event DTO (replaced by generated equivalent). |
| src/main/java/com/github/copilot/sdk/events/ToolExecutionCompleteEvent.java | Deletes legacy hand-written event DTO (replaced by generated equivalent). |
| src/main/java/com/github/copilot/sdk/events/SystemNotificationEvent.java | Deletes legacy hand-written event DTO (replaced by generated equivalent). |
| src/main/java/com/github/copilot/sdk/events/SystemMessageEvent.java | Deletes legacy hand-written event DTO (replaced by generated equivalent). |
| src/main/java/com/github/copilot/sdk/events/SubagentStartedEvent.java | Deletes legacy hand-written event DTO (replaced by generated equivalent). |
| src/main/java/com/github/copilot/sdk/events/SubagentSelectedEvent.java | Deletes legacy hand-written event DTO (replaced by generated equivalent). |
| src/main/java/com/github/copilot/sdk/events/SubagentFailedEvent.java | Deletes legacy hand-written event DTO (replaced by generated equivalent). |
| src/main/java/com/github/copilot/sdk/events/SubagentDeselectedEvent.java | Deletes legacy hand-written event DTO (replaced by generated equivalent). |
| src/main/java/com/github/copilot/sdk/events/SubagentCompletedEvent.java | Deletes legacy hand-written event DTO (replaced by generated equivalent). |
| src/main/java/com/github/copilot/sdk/events/SkillInvokedEvent.java | Deletes legacy hand-written event DTO (replaced by generated equivalent). |
| src/main/java/com/github/copilot/sdk/events/SessionWorkspaceFileChangedEvent.java | Deletes legacy hand-written event DTO (replaced by generated equivalent). |
| src/main/java/com/github/copilot/sdk/events/SessionUsageInfoEvent.java | Deletes legacy hand-written event DTO (replaced by generated equivalent). |
| src/main/java/com/github/copilot/sdk/events/SessionTruncationEvent.java | Deletes legacy hand-written event DTO (replaced by generated equivalent). |
| src/main/java/com/github/copilot/sdk/events/SessionTaskCompleteEvent.java | Deletes legacy hand-written event DTO (replaced by generated equivalent). |
| src/main/java/com/github/copilot/sdk/events/SessionStartEvent.java | Deletes legacy hand-written event DTO (replaced by generated equivalent). |
| src/main/java/com/github/copilot/sdk/events/SessionSnapshotRewindEvent.java | Deletes legacy hand-written event DTO (replaced by generated equivalent). |
| src/main/java/com/github/copilot/sdk/events/SessionShutdownEvent.java | Deletes legacy hand-written event DTO (replaced by generated equivalent). |
| src/main/java/com/github/copilot/sdk/events/SessionResumeEvent.java | Deletes legacy hand-written event DTO (replaced by generated equivalent). |
| src/main/java/com/github/copilot/sdk/events/SessionPlanChangedEvent.java | Deletes legacy hand-written event DTO (replaced by generated equivalent). |
| src/main/java/com/github/copilot/sdk/events/SessionModelChangeEvent.java | Deletes legacy hand-written event DTO (replaced by generated equivalent). |
| src/main/java/com/github/copilot/sdk/events/SessionModeChangedEvent.java | Deletes legacy hand-written event DTO (replaced by generated equivalent). |
| src/main/java/com/github/copilot/sdk/events/SessionInfoEvent.java | Deletes legacy hand-written event DTO (replaced by generated equivalent). |
| src/main/java/com/github/copilot/sdk/events/SessionIdleEvent.java | Deletes legacy hand-written event DTO (replaced by generated equivalent). |
| src/main/java/com/github/copilot/sdk/events/SessionHandoffEvent.java | Deletes legacy hand-written event DTO (replaced by generated equivalent). |
| src/main/java/com/github/copilot/sdk/events/SessionEventParser.java | Removes legacy custom event parser; switches to Jackson polymorphic deserialization on generated SessionEvent. |
| src/main/java/com/github/copilot/sdk/events/SessionErrorEvent.java | Deletes legacy hand-written event DTO (replaced by generated equivalent). |
| src/main/java/com/github/copilot/sdk/events/SessionContextChangedEvent.java | Deletes legacy hand-written event DTO (replaced by generated equivalent). |
| src/main/java/com/github/copilot/sdk/events/SessionCompactionStartEvent.java | Deletes legacy hand-written event DTO (replaced by generated equivalent). |
| src/main/java/com/github/copilot/sdk/events/SessionCompactionCompleteEvent.java | Deletes legacy hand-written event DTO (replaced by generated equivalent). |
| src/main/java/com/github/copilot/sdk/events/PermissionRequestedEvent.java | Deletes legacy hand-written event DTO (replaced by generated equivalent). |
| src/main/java/com/github/copilot/sdk/events/PermissionCompletedEvent.java | Deletes legacy hand-written event DTO (replaced by generated equivalent). |
| src/main/java/com/github/copilot/sdk/events/PendingMessagesModifiedEvent.java | Deletes legacy hand-written event DTO (replaced by generated equivalent). |
| src/main/java/com/github/copilot/sdk/events/HookStartEvent.java | Deletes legacy hand-written event DTO (replaced by generated equivalent). |
| src/main/java/com/github/copilot/sdk/events/HookEndEvent.java | Deletes legacy hand-written event DTO (replaced by generated equivalent). |
| src/main/java/com/github/copilot/sdk/events/ExternalToolRequestedEvent.java | Deletes legacy hand-written event DTO (replaced by generated equivalent). |
| src/main/java/com/github/copilot/sdk/events/ExternalToolCompletedEvent.java | Deletes legacy hand-written event DTO (replaced by generated equivalent). |
| src/main/java/com/github/copilot/sdk/events/ExitPlanModeRequestedEvent.java | Deletes legacy hand-written event DTO (replaced by generated equivalent). |
| src/main/java/com/github/copilot/sdk/events/ExitPlanModeCompletedEvent.java | Deletes legacy hand-written event DTO (replaced by generated equivalent). |
| src/main/java/com/github/copilot/sdk/events/ElicitationRequestedEvent.java | Deletes legacy hand-written event DTO (replaced by generated equivalent). |
| src/main/java/com/github/copilot/sdk/events/CommandQueuedEvent.java | Deletes legacy hand-written event DTO (replaced by generated equivalent). |
| src/main/java/com/github/copilot/sdk/events/CommandExecuteEvent.java | Deletes legacy hand-written event DTO (replaced by generated equivalent). |
| src/main/java/com/github/copilot/sdk/events/CommandCompletedEvent.java | Deletes legacy hand-written event DTO (replaced by generated equivalent). |
| src/main/java/com/github/copilot/sdk/events/CapabilitiesChangedEvent.java | Deletes legacy hand-written event DTO (replaced by generated equivalent). |
| src/main/java/com/github/copilot/sdk/events/AssistantUsageEvent.java | Deletes legacy hand-written event DTO (replaced by generated equivalent). |
| src/main/java/com/github/copilot/sdk/events/AssistantTurnStartEvent.java | Deletes legacy hand-written event DTO (replaced by generated equivalent). |
| src/main/java/com/github/copilot/sdk/events/AssistantTurnEndEvent.java | Deletes legacy hand-written event DTO (replaced by generated equivalent). |
| src/main/java/com/github/copilot/sdk/events/AssistantStreamingDeltaEvent.java | Deletes legacy hand-written event DTO (replaced by generated equivalent). |
| src/main/java/com/github/copilot/sdk/events/AssistantReasoningEvent.java | Deletes legacy hand-written event DTO (replaced by generated equivalent). |
| src/main/java/com/github/copilot/sdk/events/AssistantReasoningDeltaEvent.java | Deletes legacy hand-written event DTO (replaced by generated equivalent). |
| src/main/java/com/github/copilot/sdk/events/AssistantMessageEvent.java | Deletes legacy hand-written event DTO (replaced by generated equivalent). |
| src/main/java/com/github/copilot/sdk/events/AssistantMessageDeltaEvent.java | Deletes legacy hand-written event DTO (replaced by generated equivalent). |
| src/main/java/com/github/copilot/sdk/events/AssistantIntentEvent.java | Deletes legacy hand-written event DTO (replaced by generated equivalent). |
| src/main/java/com/github/copilot/sdk/events/AbstractSessionEvent.java | Removes legacy sealed base type in favor of generated SessionEvent. |
| src/main/java/com/github/copilot/sdk/events/AbortEvent.java | Deletes legacy hand-written event DTO (replaced by generated equivalent). |
| src/main/java/com/github/copilot/sdk/RpcHandlerDispatcher.java | Switches server-to-client event parsing to SessionEvent deserialization. |
| src/main/java/com/github/copilot/sdk/EventErrorHandler.java | Updates error handler signature to accept generated SessionEvent. |
| src/main/java/com/github/copilot/sdk/CopilotSession.java | Adds session-level typed RPC accessor, updates dispatch types to SessionEvent, and migrates many RPC calls to typed wrappers. |
| src/main/java/com/github/copilot/sdk/CopilotClient.java | Adds server-level typed RPC accessor backed by generated ServerRpc. |
| scripts/codegen/package.json | Adds Node/TS codegen package definition and dependencies. |
| scripts/codegen/java.ts | Adds the TypeScript generator for Java event classes, RPC DTO records, and RPC wrapper APIs. |
| scripts/codegen/.gitignore | Ignores codegen node_modules/. |
| pom.xml | Adds src/generated/java as a source root and excludes generated sources from Spotless formatting. |
| jbang-example.java | Updates example imports to the generated event package. |
| docs/WORKFLOWS.md | Documents new Codegen Check and dependency bump workflows. |
| config/spotbugs/spotbugs-exclude.xml | Updates SpotBugs exclusions to cover generated DTOs. |
| config/checkstyle/checkstyle.xml | Excludes generated sources from Checkstyle enforcement. |
| README.md | Updates getting-started snippet imports to generated event types. |
| .github/workflows/update-copilot-dependency.yml | Adds workflow to bump @github/copilot, re-run codegen, and open a PR. |
| .github/workflows/codegen-check.yml | Adds workflow to ensure generated files are in sync with the generator output. |
| .github/prompts/coding-agent-merge-reference-impl-instructions.md | Adds a “do not edit generated sources” guardrail for sync automation. |
| .github/prompts/agentic-merge-reference-impl.prompt.md | Adds an absolute prohibition against modifying src/generated/java/**. |
| .github/copilot-instructions.md | Updates repo architecture docs to point to generated events package. |
| .gitattributes | Marks generated Java sources as generated and enforces LF endings. |
Copilot's findings
Files not reviewed (1)
- scripts/codegen/package-lock.json: Language not supported
Comments suppressed due to low confidence (1)
scripts/codegen/java.ts:1
- RpcMapper creates a fresh default ObjectMapper, which will not have the same module/configuration as the SDK’s runtime mapper (notably JavaTimeModule). Any session RPC wrapper method that uses MAPPER.valueToTree(params) can fail at runtime when params contain OffsetDateTime (and potentially other configured types). Generate RpcMapper so it matches the JsonRpcClient ObjectMapper configuration (e.g., register JavaTimeModule and any deserialization/serialization features used elsewhere), or change wrapper generation to avoid re-serializing params via a separate mapper.
/*---------------------------------------------------------------------------------------------
- Files reviewed: 83/329 changed files
- Comments generated: 5
…ent wire type, anyOf heuristic, remove unused var Agent-Logs-Url: https://github.com/github/copilot-sdk-java/sessions/9b8b782c-22ad-450f-885d-2b11d5808a0c Co-authored-by: edburns <75821+edburns@users.noreply.github.com>
Collaborator
Author
Contributor
6 tasks
…essionEvent wire type, anyOf heuristic, remove unused var" This reverts commit ef1de83.
… (HIGH) scripts/codegen/java.ts - Remove the special-case `anyOf` rule that picked `String` when exactly 2 non-null branches included a string type. Multi-branch `anyOf` now falls through to `Object`, matching the C# reference generator's behavior. src/generated/java/com/github/copilot/sdk/generated/rpc/SessionToolsHandlePendingToolCallParams.java - Change the `result` field type from `String` to `Object` (regenerated output reflecting the `anyOf` rule fix in `java.ts`). src/main/java/com/github/copilot/sdk/CopilotSession.java - Replace the `sendExpandedToolResult(requestId, toolResult)` call with a direct typed-wrapper call: `getRpc().tools.handlePendingToolCall(new SessionToolsHandlePendingToolCallParams(...))`. - Delete the `sendExpandedToolResult()` private method and its Javadoc block, which are no longer needed now that the `result` field accepts `Object`. Signed-off-by: Ed Burns <edburns@microsoft.com>
modified: scripts/codegen/java.ts - Put `visible = true` on `SessionEventEvent`. - Add `type` property. on `UnknownSessionEvent`. modified: src/generated/java/com/github/copilot/sdk/generated/SessionEvent.java modified: src/generated/java/com/github/copilot/sdk/generated/UnknownSessionEvent.java - Regenerated. modified: src/main/java/com/github/copilot/sdk/CopilotSession.java - Use Double Check Locked to fix Big Risk #2 2. Lazy `SessionRpc` initialization is not thread-safe (HIGH) modified: src/test/java/com/github/copilot/sdk/ForwardCompatibilityTest.java modified: src/test/java/com/github/copilot/sdk/SessionEventDeserializationTest.java - Refine tests based on changes. Signed-off-by: Ed Burns <edburns@microsoft.com>
modified: pom.xml - Add profiles for generating code and updating the schemas from which the code is generated. modified: scripts/codegen/java.ts - Address copilot comment: > requiredSet is computed but never used in renderNestedType(), which makes the generator harder to maintain (and can confuse future changes around nullability/boxing). Remove it or use it to drive required-vs-optional component typing if that’s the intent. Signed-off-by: Ed Burns <edburns@microsoft.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #69
Supersedes #70
Reviewer's Guide — PR #79
The Big Idea
This PR introduces a TypeScript-based code generator (
scripts/codegen/java.ts) that reads the Copilot CLI's JSON schema files (api.schema.json,session-events.schema.json) and produces strongly-typed Java source files undersrc/generated/java/. The generated code replaces two categories of hand-written code:Session event classes — A sealed
SessionEventhierarchy (~70 event types) with Jackson@JsonSubTypespolymorphic deserialization, replacing the hand-writtencom.github.copilot.sdk.eventspackage that is deleted in this PR.Typed RPC wrapper classes —
ServerRpcandSessionRpcwith namespace sub-APIs (e.g.,session.tools,session.ui,session.model), plus ~130 param/result record DTOs. These replace rawrpc.invoke("method.name", Map.of(...), ...)calls throughoutCopilotClientandCopilotSession.After this PR, internal RPC call sites in
CopilotSession(tool results, permission handling, command handling, elicitation, model switching, logging) use typed wrappers like:Both
CopilotClient.getRpc()andCopilotSession.getRpc()are public API, so SDK consumers can call any RPC method the CLI exposes — not just those the SDK wraps with convenience methods.Supporting infrastructure
codegen-check.yml— Runs the generator and fails if the committed files diverge from what the generator produces.update-copilot-dependency.yml— Manualworkflow_dispatchthat bumps@github/copilotinscripts/codegen/package.json, regenerates all files, and opens a PR.agentic-merge-reference-impl.prompt.md) now includes an ABSOLUTE PROHIBITION preventing the sync agent from hand-editing anything undersrc/generated/java/.The Big Risks
1.
sendExpandedToolResult()bypasses the typed wrapper (HIGH)File:
CopilotSession.java, line ~871The generated
SessionToolsHandlePendingToolCallParams.resultfield is typed asString(the code generator picksStringforanyOf[string, object]), but the protocol requires a JSON object (ToolResultObject) for success results. The workaround issendExpandedToolResult(), which hand-builds anObjectNodeand callsrpc.invoke(...)directly.What to verify: This is the single place where the typed wrapper is intentionally bypassed. If the protocol ever changes the shape of the
resultfield, this hand-built node will silently produce invalid payloads. Confirm theObjectNodeconstruction (sessionId,requestId,resultas serialized tree) matches what the server expects. Also consider whether the generator'sanyOfresolution rule should be improved in a follow-up.2. Lazy
SessionRpcinitialization is not thread-safe (HIGH)File:
CopilotSession.java, lines ~300–310sessionRpcis declaredvolatile, but the lazy init ingetRpc()does a check-then-assign without synchronization:Two threads calling
getRpc()simultaneously could both seenulland each create a separateSessionRpcinstance. BecauseSessionRpcis stateless (captures onlycaller+sessionId), duplicating it is harmless — but confirm no state is accumulated on the instance.Additionally,
setActiveSessionId()setssessionRpc = nullto force re-creation on the nextgetRpc()call. This is safe only becauseSessionRpcdoesn't cache anything mutable.3. ~200 generated files committed as-is — spot-check against schema (MEDIUM)
The
src/generated/java/tree contains ~70 event classes and ~130 RPC param/result/API classes. They are produced byscripts/codegen/java.tsand committed verbatim. The CIcodegen-check.ymlworkflow verifies they stay in sync with the generator, but does not verify the generator is correct relative to the schema.What to verify: Spot-check 2–3 generated API classes (e.g.,
SessionToolsApi.java,SessionUiApi.java) against the corresponding methods inapi.schema.json. Confirm the parameter record fields match the JSON schema properties (names, types, required vs optional). The generator output is 100% deterministic, so checking a few representative classes is sufficient.Fixed in https://github.com/github/copilot-sdk-java/tree/copilot/add-classical-code-gen-workflow-ready-add-copilot-review-to-codegen-step
4. Enum
fromValue()is case-sensitive (MEDIUM)File:
SessionUiElicitationResult.java(and all generated enums)Every generated enum has a
@JsonCreator fromValue(String)method that does an exactv.value.equals(value)comparison. If the server ever sends"Accept"or"ACCEPT"instead of"accept", deserialization will throwIllegalArgumentException.PENDING(edburns): See https://github.slack.com/archives/C09TX0PUJ3Z/p1776456451925629
What to verify: Confirm the Copilot CLI consistently sends lowercase enum values. The comparison in
CopilotSessionuses==on enum constants (e.g.,resp.action() == SessionUiElicitationResultAction.ACCEPT), which is correct — the risk is entirely in the deserialization path.Gotchas
ServerRpcis eager;SessionRpcis lazy — by designServerRpcis created once, eagerly, in theConnectionrecord whenCopilotClient.start()connects.SessionRpcis created lazily on firstCopilotSession.getRpc()call because thesessionIdisn't known untilcreateSession()returns. This asymmetry is intentional, not accidental.Generated RPC wrappers inject
sessionIdviaObjectNodemutationEvery method in
SessionRpcand its sub-API classes serializes the param record to anObjectNode, then calls_p.put("sessionId", this.sessionId)to inject the session ID. This means thesessionIdfield in param records (e.g.,SessionToolsHandlePendingToolCallParams.sessionId) is effectively overwritten. Call sites still passsessionIdin the constructor for readability, but the generated wrapper stomps it.Tests now fail instead of skip when CLI is absent
The old pattern was
if (cliPath == null) { return; }which silently skipped. The newTestUtilusesassertNotNull(cliPath, ...)which fails. This is intentional — CI must have the CLI installed.The
events→generatedpackage move changes all import pathsEvery test and production file that referenced
com.github.copilot.sdk.events.SomethingEventnow importscom.github.copilot.sdk.generated.SomethingEvent. TheSessionEventsealed class (which wasAbstractSessionEventin the old package) is nowSessionEvent— a class rename, not just a move.